package com.xzx.paymentdemo.service.impl;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.google.gson.Gson;
import com.xzx.paymentdemo.config.WxPayConfig;
import com.xzx.paymentdemo.entity.OrderInfo;
import com.xzx.paymentdemo.entity.RefundInfo;
import com.xzx.paymentdemo.enums.OrderStatus;
import com.xzx.paymentdemo.enums.wxpay.WxApiType;
import com.xzx.paymentdemo.enums.wxpay.WxNotifyType;
import com.xzx.paymentdemo.enums.wxpay.WxRefundStatus;
import com.xzx.paymentdemo.enums.wxpay.WxTradeState;
import com.xzx.paymentdemo.service.OrderInfoService;
import com.xzx.paymentdemo.service.PaymentInfoService;
import com.xzx.paymentdemo.service.RefundInfoService;
import com.xzx.paymentdemo.service.WxPayService;
import com.xzx.paymentdemo.util.OrderNoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.core.annotation.OrderUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 * .
 *
 * @author xzx
 * @since 2022/8/11
 */
@Service
@Slf4j
public class WxPayServiceImpl implements WxPayService {

    @Resource
    private WxPayConfig wxPayConfig;
    @Resource
    private CloseableHttpClient wxPayClient;
    @Resource
    private OrderInfoService orderInfoService;
    @Resource
    private PaymentInfoService paymentInfoService;
    @Resource
    private RefundInfoService refundInfoService;
    @Resource
    private CloseableHttpClient wxPayNoSignClient;

    private final ReentrantLock lock = new ReentrantLock();

    /**
     * 创建订单，调用Native支付接口
     *
     * @param productId
     * @return code_url 和 订单号
     * @throws Exception
     */
    //@Transactional(rollbackFor = Exception.class)
    @Override
    public Map<String, Object> nativePay(Long productId) throws IOException {
        log.info("生成订单");
        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);

        String codeUrl = orderInfo.getCodeUrl();
        if (!StringUtils.isBlank(codeUrl)) {
            log.info("订单已存在，二维码已保存");
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        }
        String jsonParams = generateJsonParamsOfNativePay(orderInfo);
        log.info("请求参数 ===> {}" + jsonParams);
        String url = wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType());//Mative下单的url
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        log.info("调用统一下单API");
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            codeUrl = resultMap.get("code_url");
            //保存二维码
            orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl", codeUrl);
            map.put("orderNo", orderInfo.getOrderNo());
            return map;
        } finally {
            response.close();
        }
    }

    /**
     * 处理订单
     *
     * @param decryptData 解密后的报文
     */
    @Override
    public void processOrder(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前，
        要采用数据锁进行并发控制，
        以避免函数重入造成的数据混乱*/
        //尝试获取锁：
        // 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                //下单后的OrderStatus == WxTradeState.NOTPAY
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
                //记录支付日志
                paymentInfoService.createPaymentInfo(decryptData);
            } finally {
                lock.unlock();
            }
        }

    }

    /**
     * 微信关单,更新数据库
     *
     * @param orderNo
     * @throws IOException
     */
    @Override
    public void cancelOrder(String orderNo) throws IOException {
        //调用微信支付的关单接口 
        closeOrder(orderNo);
        // 更新商户端的订单状态为取消
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);
    }

    /**
     * 根据订单号查询微信支付查单接口
     *
     * @param orderNo
     * @return 响应体的明文
     * @throws IOException
     */
    @Override
    public String queryOrder(String orderNo) throws IOException {
        log.info("查单接口调用 ===> {}", orderNo);
        //组装请求URL
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("查询订单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }
    }

    /**
     * 根据订单号查询微信支付查单接口，核实订单状态
     * 如果订单已支付，则更新商户端订单状态，并记录支付日志
     * 如果订单未支付，则调用关单接口关闭订单，并更新商户端订单状态
     *
     * @param orderNo 创建5分钟且未支付的订单号
     */
    @Override
    public void checkOrderStatus(String orderNo) throws IOException {
        log.warn("根据订单号核实订单状态 ===> {}", orderNo);
        Gson gson = new Gson();
        //查单
        String bodyAsString = queryOrder(orderNo);
        Map<String, Object> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String trade_state = (String) bodyMap.get("trade_state");
        if (WxTradeState.SUCCESS.getType().equals(trade_state)) {
            log.warn("核实订单已支付 ===> {}", orderNo);
            //如果确认订单已支付则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(bodyAsString);
        } else if (WxTradeState.NOTPAY.getType().equals(trade_state)) {
            log.warn("核实订单未支付 ===> {}", orderNo);
            //关单接口
            closeOrder(orderNo);
            //更新为超时关闭
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        } else if (WxTradeState.REFUND.getType().equals(trade_state)) {
            log.warn("核实订单以退款 ===> {}", orderNo);
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
            refundInfoService.updateRefund(bodyAsString);
        }
    }

    /**
     * 申请退款
     *
     * @param orderNo
     * @param reason
     */
    @Override
    public void refund(String orderNo, String reason) throws IOException {
        log.info("创建退款单记录");
        RefundInfo refundInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);
        log.info("调用退款API");
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        //json请求体
        String jsonParams = generateJsonParamsOfRefund(refundInfo);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);

        try {
            //获取响应体
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                throw new RuntimeException("退款失败,响应码 = " + statusCode + ",退款返回结果 = " + bodyAsString);
            }
            //更新订单状态--正在处理退款
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);
            //更新退款单
            refundInfoService.updateRefund(bodyAsString);
        } finally {
            response.close();
        }
    }

    /**
     * 调用查询退款单接口，核实订单状态
     *
     * @param refundNo
     * @return
     * @throws IOException
     */
    @Override
    public String queryRefund(String refundNo) throws IOException {
        log.info("查询退款接口调用 ===> {}", refundNo);
        String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);
        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("查询退款失败,响应码 = " + statusCode + ",查询退款返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            return bodyAsString;
        } finally {
            response.close();
        }

    }

    /**
     * 1.调用查询退款记录接口
     * 2.更新数据库
     *
     * @param refundNo
     * @throws IOException
     */
    @Override
    public void checkRefundStatus(String refundNo) throws IOException {
        log.warn("根据退款订单号核实退款订单状态 ===> {}", refundNo);
        Gson gson = new Gson();
        //查询退款订单
        String bodyAsString = queryRefund(refundNo);
        Map<String, String> bodyMap = gson.fromJson(bodyAsString, HashMap.class);
        String status = bodyMap.get("status");
        String orderNo = bodyMap.get("out_trade_no");
        if (WxRefundStatus.SUCCESS.getType().equals(status)) {
            log.warn("核实订单退款成功 ===> {}", refundNo);
            //如果确认订单已退款成功则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
        } else if (WxRefundStatus.ABNORMAL.getType().equals(status)) {
            log.warn("核实订单退款异常 ===> {}", refundNo);
            //如果确认订单退款异常则更新本地订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);
        }
        //保存响应体到数据库中
        refundInfoService.updateRefund(bodyAsString);
    }

    /**
     * 处理退款通知
     *
     * @param decryptData 解密后的明文
     */
    @Override
    public void processRefund(String decryptData) {
        Gson gson = new Gson();
        Map<String, Object> decryptDataMap = gson.fromJson(decryptData, HashMap.class);
        String orderNo = (String) decryptDataMap.get("out_trade_no");
        /*在对业务数据进行状态检查和处理之前，
        要采用数据锁进行并发控制，
        以避免函数重入造成的数据混乱*/
        //尝试获取锁：
        // 成功获取则立即返回true，获取失败则立即返回false。不必一直等待锁的释放
        if (lock.tryLock()) {
            try {
                //处理重复通知
                //保证接口调用的幂等性：无论接口被调用多少次，产生的结果是一致的
                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                //申请退款后的OrderStatus == REFUND_PROCESSING
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }
                //更新订单状态 -- 退款成功
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);
                //将回调通知的请求体存入数据库
                refundInfoService.updateRefund(decryptData);
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * 申请账单
     *
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) throws IOException {
        log.warn("申请账单接口调用 {}", billDate);
        String url = "";
        if ("tradebill".equals(type)) {
            url = WxApiType.TRADE_BILLS.getType();
        } else if ("fundflowbill".equals(type)) {
            url = WxApiType.FUND_FLOW_BILLS.getType();
        } else {
            throw new RuntimeException("不支持的账单类型");
        }

        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 申请账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("申请账单异常,响应码 = " + statusCode + ",申请账单异常返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }
            //获取账单下载地址
            Gson gson = new Gson();
            Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
            return resultMap.get("download_url");
        } finally {
            response.close();
        }
    }

    /**
     * 下载账单
     * @param billDate
     * @param type
     * @return
     */
    @Override
    public String downloadBill(String billDate, String type) throws IOException {
        log.warn("下载账单接口调用 {}, {}", billDate, type);
        String downloadUrl = queryBill(billDate, type);

        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");
        //完成签名并执行请求,无需验签响应结果
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) { //处理成功
                log.info("成功, 下载账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功");
            } else {
                log.info("下载账单异常,响应码 = " + statusCode + ",下载账单异常返回结果 = " + bodyAsString);
                throw new IOException("request failed");
            }

            return bodyAsString;
        } finally {
            response.close();
        }
    }

    /**
     * 调用关单接口
     * @param orderNo
     * @throws IOException
     */
    private void closeOrder(String orderNo) throws IOException {
        log.info("关单接口的调用，订单号 ===> {}", orderNo);
        //创建远程请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);//取消订单url
        //组装json请求体
        Gson gson = new Gson();
        Map<String, String> paramsMap = new HashMap();
        paramsMap.put("mchid", wxPayConfig.getMchId());
        //将参数转换成json字符串
        String jsonParams = gson.toJson(paramsMap);
        HttpPost httpPost = generateHttpPost(jsonParams, url);
        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        try {
            if (statusCode == 200) { //处理成功
                log.info("成功200");
            } else if (statusCode == 204) { //处理成功，无返回Body
                log.info("成功204");
            } else {
                log.info("取消订单失败,响应码 = " + statusCode);
                throw new IOException("request failed");
            }
        } finally {
            response.close();
        }
    }

    /**
     * 生成HttpPost
     *
     * @param jsonParams
     * @param url
     * @return
     */
    private HttpPost generateHttpPost(String jsonParams, String url) {
        //拼接请求URL
        HttpPost httpPost = new HttpPost(url);
        //设置请求头，编码集，内容格式
        StringEntity entity = new StringEntity(jsonParams, "utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);
        httpPost.setHeader("Accept", "application/json");
        return httpPost;
    }


    /**
     * 生成下单请求体json
     *
     * @param orderInfo
     * @return
     */
    private String generateJsonParamsOfNativePay(OrderInfo orderInfo) {
        Gson gson = new Gson();
        HashMap paramsMap = new HashMap();
        paramsMap.put("appid", wxPayConfig.getAppid());
        paramsMap.put("mchid", wxPayConfig.getMchId());
        paramsMap.put("description", orderInfo.getTitle());
        paramsMap.put("out_trade_no", orderInfo.getOrderNo());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

        Map amountMap = new HashMap();
        amountMap.put("total", orderInfo.getTotalFee());
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);

        //将参数转换成json字符串
        return gson.toJson(paramsMap);
    }

    /**
     * 生成退款请求体json
     * @param refundInfo
     * @return
     */
    private String generateJsonParamsOfRefund(RefundInfo refundInfo) {
        Gson gson = new Gson();
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("out_trade_no", refundInfo.getOrderNo());//商品订单号
        paramsMap.put("out_refund_no", refundInfo.getRefundNo());//退款单号
        paramsMap.put("reason", refundInfo.getReason());
        paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));
        Map<String, Object> amountMap = new HashMap<>();
        amountMap.put("refund", refundInfo.getRefund());//退款金额
        amountMap.put("total", refundInfo.getTotalFee());//原订单金额
        amountMap.put("currency", "CNY");
        paramsMap.put("amount", amountMap);
        //将参数转换成json字符串
        return gson.toJson(paramsMap);
    }

}
